/**
 * @description 关于 Pinia 和远程数据源的工具函数集
 */
import type { Ref } from 'vue';

export interface DefineDataOptions {
  /**
   * @description 是否在首次访问 `data` 时调用 `reloadFn`
   * @default false
   */
  loadOnFirstVisit?: boolean;

  /**
   * @description 是否立即使用 `reloadFn` 加载一次数据
   * @default false
   */
  loadImmediately?: boolean;

  /**
   * @description 是否在 `reloadFn` 失败后重置数据为 `initValue`
   * @default false
   */
  resetOnError?: boolean;
}

export interface DefineDataReturn<T> {
  /**
   * @description 数据
   */
  data: Ref<T>;

  /**
   * @description 是否正在加载数据
   */
  loading: Ref<boolean>;

  /**
   * @description 使用 `reloadFn` 刷新数据
   */
  reload: () => Promise<void>;

  /**
   * @description 重置数据为 `initValue`
   */
  reset: () => void;

  /**
   * @description 若 `reloadFn` 刷新数据失败 (返回的 Promise 被拒绝)，拒绝原因会被存放在这里; 成功刷新一次后会重置为 `undefined`
   */
  err: Ref<any | undefined>;
}

const defaultOptions: DefineDataOptions = {
  loadOnFirstVisit: false,
  loadImmediately: false,
  resetOnError: false,
};

/**
 * 定义一个数据源，封装了数据、状态和它的刷新逻辑
 * @param initValue 初始值
 * @param reloadFn 刷新数据的函数
 * @param options 配置项
 * @returns 返回一个 DefineDataReturn 对象
 */
export function defineDataSource<T>(
  initValue: T,
  reloadFn: () => Promise<T>,
  options: DefineDataOptions = defaultOptions
): DefineDataReturn<T> {
  // NOTE: _data 的实际类型是 Ref<UnwrapRef<T>>，这意味着如果 initValue 中包含响应式数据，会被 unwrap， 此时返回的 data 的内部类型可能和 T 不一致
  // see: https://github.com/vuejs/core/issues/1324
  const _data = ref(initValue) as Ref<T>; // 底层数据
  const _inited = ref(true); // 是否已初始化，用于支持首次访问初始化
  if (options.loadOnFirstVisit) {
    _inited.value = false;
  }

  const data = computed<T>({
    get: () => {
      if (!_inited.value) {
        _inited.value = true;
        reload().catch(() => {});
      }
      return _data.value;
    },
    set: (v: T) => {
      _data.value = v;
    },
  });
  const loading = ref(false); // 加载中
  const err = ref<any>(); // 错误信息

  function reset() {
    data.value = initValue;
  }

  function reload(): Promise<void> {
    loading.value = true;
    return reloadFn()
      .then((newData) => {
        data.value = newData;
        err.value = undefined;
      })
      .catch((e) => {
        err.value = e;
        if (options.resetOnError) reset();
        throw e; // 重新抛出错误
      })
      .finally(() => {
        loading.value = false;
      });
  }

  if (options.loadImmediately) {
    reload().catch(() => {});
  }

  return { data, loading, reload, reset, err };
}
